Lexer.js ➔ ???   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 0 Features 0
Metric Value
c 11
b 0
f 0
nc 1
dl 0
loc 17
rs 9.4285
cc 2
nop 1
1
'use strict';
2
3
import Input from './Input';
4
import reservedKeywords from '../../lang/ReservedKeywords.json';
5
import Token from './Token';
6
7
/**
8
 * The botlang lexer takes an instance of `Input` as a constructor argument and produces
9
 * a token stream for further processing in the parser.
10
 */
11
class Lexer {
12
  /**
13
   * Create a Lexer.
14
   * @param {Input} input
15
   * @throws {TypeError}
16
   * @return {void}
17
   */
18
  constructor(input) {
19
    if (!(input instanceof Input)) {
20
      throw new TypeError('Argument "input" must be an instance of type "Input".');
21
    }
22
23
    /**
24
     * @private
25
     * @type {Token}
26
     */
27
    this.currentToken = null;
28
29
    /**
30
     * @private
31
     * @type {Input}
32
     */
33
    this.input = input;
34
  }
35
36
  /**
37
   * Determine whether or not the lexer has reached the input's eof
38
   * @return {Boolean}
39
   */
40
  eof() {
41
    return this.input.eof();
42
  }
43
44
  /**
45
   * Throw a new Error.
46
   * @param  {String} message
47
   * @throws {Error}
48
   * @return {void}
49
   */
50
  inputError(message) {
51
    this.input.error(message);
52
  }
53
54
  /**
55
   * @private
56
   * @param {String} char
57
   * @return {Boolean}
58
   */
59
  static isDigit(char) {
60
    return /[0-9]/.test(char);
61
  }
62
63
  /**
64
   * @private
65
   * @param {String} char
66
   * @return {Boolean}
67
   */
68
  static isIdentifier(char) {
69
    return Lexer.isIdentifierStart(char) || /[a-zA-Z_]/.test(char);
70
  }
71
72
  /**
73
   * @private
74
   * @param {String} char
75
   * @return {Boolean}
76
   */
77
  static isIdentifierStart(char) {
78
    return /[a-zA-Z]/.test(char);
79
  }
80
81
  /**
82
   * @private
83
   * @param {String} char
84
   * @return {Boolean}
85
   */
86
  static isOperation(char) {
87
    return -1 !== '+-*/%=&|<>!'.indexOf(char);
88
  }
89
90
  /**
91
   * @private
92
   * @param {String} char
93
   * @return {Boolean}
94
   */
95
  static isPunctuation(char) {
96
    return -1 !== ',;(){}[]'.indexOf(char);
97
  }
98
99
  /**
100
   * @private
101
   * @param {String} char
102
   * @return {Boolean}
103
   */
104
  static isReservedKeyword(char) {
105
    return -1 !== reservedKeywords.indexOf(char);
106
  }
107
108
  /**
109
   * @private
110
   * @param {String} char
111
   * @return {Boolean}
112
   */
113
  static isString(char) {
114
    return '"' === char;
115
  }
116
117
  /**
118
   * @private
119
   * @param {String} char
120
   * @return {Boolean}
121
   */
122
  static isWhitespace(char) {
123
    return /\s/.test(char);
124
  }
125
126
  /**
127
   * Return the next token in from the input stream or `null` if the end of file has been reached
128
   * @return {Token|null}
129
   */
130
  next() {
131
    const token = this.currentToken;
132
    this.currentToken = null;
133
134
    return token || this.nextToken();
135
  }
136
137
  /**
138
   * @private
139
   * @return {Token|null}
140
   */
141
  nextToken() {
142
    // Ignore whitespace
143
    this.readWhile(Lexer.isWhitespace);
144
145
    // Return if is end of stream
146
    if (this.input.eof()) {
147
      return null;
148
    }
149
150
    // Get the current character
151
    const char = this.input.peek();
152
153
    // Ignore comments
154
    if ('#' === char) {
155
      this.skipComment();
156
      return this.nextToken();
157
    }
158
159
    // Read string
160
    if (Lexer.isString(char)) {
161
      return this.readString();
162
    }
163
164
    // Read number
165
    if (Lexer.isDigit(char)) {
166
      return this.readNumber();
167
    }
168
169
    // Read identifier
170
    if (Lexer.isIdentifierStart(char)) {
171
      return this.readIdentifier();
172
    }
173
174
    // Read operation
175
    if (Lexer.isOperation(char)) {
176
      return this.readOperation();
177
    }
178
179
    // Read punctuation
180
    if (Lexer.isPunctuation(char)) {
181
      return this.readPunctuation();
182
    }
183
184
    return this.inputError('Invalid character');
185
  }
186
187
  /**
188
   * Return the current token or get the first token if `next` has not been called yet.
189
   * @return {Token}
190
   */
191
  peek() {
192
    return this.currentToken || this.next();
193
  }
194
195
  /**
196
   * @private
197
   * @return {String}
198
   */
199
  readEscaped(end) {
200
    let escaped = false,
201
        str = '';
202
203
    while (!this.input.eof()) {
204
      const char = this.input.next();
205
206
      if (escaped) {
207
        str += char;
208
        escaped = false;
209
      } else if ('\\' === char) {
210
        escaped = true;
211
      } else if (char === end) {
212
        this.input.next();
213
        break;
214
      } else {
215
        str += char;
216
      }
217
    }
218
219
    return str;
220
  }
221
222
  /**
223
   * @private
224
   * @return {Token}
225
   */
226
  readIdentifier() {
227
    const identifier = this.input.peek().concat(
228
      this.readWhile(char => Lexer.isIdentifier(char))
229
    ).trim();
230
231
    return new Token(
232
      Lexer.isReservedKeyword(identifier) ? 'keyword' : 'identifier',
233
      identifier
234
    );
235
  }
236
237
  /**
238
   * @private
239
   * @return {Token}
240
   */
241
  readNumber() {
242
    let isDecimal = false;
243
244
    const number = this.input.peek().concat(this.readWhile((char) => {
245
      if ('.' === char) {
246
        if (isDecimal) {
247
          return false;
248
        }
249
        isDecimal = true;
250
        return true;
251
      }
252
      return Lexer.isDigit(char);
253
    })).trim();
254
255
    return new Token('numeric', parseFloat(number));
256
  }
257
258
  /**
259
   * @private
260
   * @return {Token}
261
   */
262
  readOperation() {
263
    return new Token(
264
      'operation',
265
      this.input.peek().concat(this.readWhile(char => Lexer.isOperation(char))).trim()
266
    );
267
  }
268
269
  /**
270
   * @private
271
   * @return {Token}
272
   */
273
  readPunctuation() {
274
    return new Token('punctuator', this.input.peek());
275
  }
276
277
  /**
278
   * @private
279
   * @return {Token}
280
   */
281
  readString() {
282
    return new Token('string', this.readEscaped('"'));
283
  }
284
285
  /**
286
   * @private
287
   * @param  {Function} callback
288
   * @return {String}
289
   */
290
  readWhile(callback) {
291
    let str = '';
292
293
    while (!this.input.eof() && callback(this.input.peek())) {
294
      str += this.input.next();
295
    }
296
297
    return str;
298
  }
299
300
  /**
301
   * @private
302
   */
303
  skipComment() {
304
    this.readWhile(char => '\n' !== char);
305
306
    this.input.next();
307
  }
308
}
309
310
export default Lexer;
311